package com.pig4cloud.pig.auth.support;

import org.springframework.lang.Nullable;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.token.*;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.io.Serial;
import java.time.Instant;
import java.util.*;

/**
 * 自定义 OAuth2 访问令牌生成器
 * <p>
 * 该类实现了 OAuth2TokenGenerator 接口，用于生成不透明的访问令牌（Opaque Token）。
 * 与 JWT令牌不同，不透明令牌是一个随机字符串，不包含任何用户信息。
 * 
 * 主要功能：
 * 1. 生成 96 位的 Base64 URL 安全编码随机令牌
 * 2. 构建令牌声明集（claims）
 * 3. 支持令牌自定义器（customizer）扩展
 * 4. 设置令牌的基本属性（过期时间、受众、作用域等）
 * 
 * 使用场景：
 * - 当需要生成不透明令牌而非 JWT 时
 * - 令牌信息存储在服务端，客户端只持有引用
 * - 需要更高的安全性和可控性
 * 
 * @author lengleng
 * @date 2025/05/30
 */
public class CustomeOAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OAuth2AccessToken> {

	/**
	 * 访问令牌自定义器
	 * 用于在生成令牌时添加额外的声明或修改现有声明
	 */
	private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;

	/**
	 * 访问令牌字符串生成器
	 * 使用 Base64 URL 安全编码，生成 96 位随机字符串
	 * 无填充字符，适合在 URL 中传输
	 */
	private final StringKeyGenerator accessTokenGenerator = new Base64StringKeyGenerator(
			Base64.getUrlEncoder().withoutPadding(), 96);

	/**
	 * 生成 OAuth2 访问令牌
	 * <p>
	 * 生成流程：
	 * 1. 检查令牌类型是否为 ACCESS_TOKEN
	 * 2. 检查令牌格式是否为 REFERENCE（不透明令牌）
	 * 3. 构建令牌声明集，包括基本信息
	 * 4. 调用自定义器添加额外声明
	 * 5. 生成最终的访问令牌
	 * 
	 * @param context OAuth2 令牌上下文，包含生成令牌所需的所有信息
	 * @return 生成的访问令牌，如果令牌类型不是 ACCESS_TOKEN 或格式不是 REFERENCE 则返回 null
	 * @see OAuth2TokenContext
	 * @see OAuth2AccessToken
	 */
	@Nullable
	@Override
	public OAuth2AccessToken generate(OAuth2TokenContext context) {
		// 只处理访问令牌类型且格式为引用类型（不透明令牌）
		if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) || !OAuth2TokenFormat.REFERENCE
			.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {
			return null;
		}

		// 获取发行者信息
		String issuer = null;
		if (context.getAuthorizationServerContext() != null) {
			issuer = context.getAuthorizationServerContext().getIssuer();
		}
		RegisteredClient registeredClient = context.getRegisteredClient();

		// 计算令牌时间
		Instant issuedAt = Instant.now();
		Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());

		// 构建令牌声明集
		// 包括：发行者、主体、受众、时间信息、唯一标识符等
		// @formatter:off
        OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
        if (StringUtils.hasText(issuer)) {
            claimsBuilder.issuer(issuer);  // 设置发行者
        }
        claimsBuilder
                .subject(context.getPrincipal().getName())  // 设置主体（用户名）
                .audience(Collections.singletonList(registeredClient.getClientId()))  // 设置受众（客户端 ID）
                .issuedAt(issuedAt)  // 设置签发时间
                .expiresAt(expiresAt)  // 设置过期时间
                .notBefore(issuedAt)  // 设置生效时间
                .id(UUID.randomUUID().toString());  // 设置唯一标识符
        if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
            claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());  // 设置授权范围
        }
        // @formatter:on

		// 如果设置了自定义器，调用自定义器添加额外的声明
		if (this.accessTokenCustomizer != null) {
			// 构建自定义器上下文
			// @formatter:off
            OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
                    .registeredClient(context.getRegisteredClient())
                    .principal(context.getPrincipal())
                    .authorizationServerContext(context.getAuthorizationServerContext())
                    .authorizedScopes(context.getAuthorizedScopes())
                    .tokenType(context.getTokenType())
                    .authorizationGrantType(context.getAuthorizationGrantType());
            if (context.getAuthorization() != null) {
                accessTokenContextBuilder.authorization(context.getAuthorization());
            }
            if (context.getAuthorizationGrant() != null) {
                accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
            }
            // @formatter:on

			OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
			// 调用自定义器，允许添加额外的声明
			this.accessTokenCustomizer.customize(accessTokenContext);
		}

		// 生成最终的令牌声明集
		OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();
		// 创建并返回不透明访问令牌
		// 令牌值是随机生成的字符串，不包含用户信息
		return new CustomeOAuth2AccessTokenGenerator.OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER,
				this.accessTokenGenerator.generateKey(), accessTokenClaimsSet.getIssuedAt(),
				accessTokenClaimsSet.getExpiresAt(), context.getAuthorizedScopes(), accessTokenClaimsSet.getClaims());
	}

	/**
	 * 设置用于定制 {@link OAuth2AccessToken} 的 {@link OAuth2TokenClaimsContext#getClaims()} 的 {@link OAuth2TokenCustomizer}
	 * <p>
	 * 通过该方法可以注入一个自定义器，用于在生成令牌时添加额外的业务信息。
	 * 例如：用户 ID、部门信息、权限列表等。
	 * 
	 * @param accessTokenCustomizer 用于定制 {@code OAuth2AccessToken} 声明的 {@link OAuth2TokenCustomizer}
	 * @throws IllegalArgumentException 当 accessTokenCustomizer 为 null 时抛出
	 */
	public void setAccessTokenCustomizer(OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer) {
		Assert.notNull(accessTokenCustomizer, "accessTokenCustomizer cannot be null");
		this.accessTokenCustomizer = accessTokenCustomizer;
	}

	/**
	 * OAuth2 访问令牌声明类
	 * <p>
	 * 该内部类继承自 OAuth2AccessToken 并实现 ClaimAccessor 接口，
	 * 用于将令牌声明（claims）与访问令牌关联起来。
	 * 
	 * 主要作用：
	 * 1. 封装访问令牌的基本信息（类型、值、时间、作用域）
	 * 2. 携带令牌的声明信息（claims）
	 * 3. 提供获取声明信息的接口
	 * 
	 * 注意：虽然令牌本身是不透明的，但声明信息会存储在服务端
	 *
	 * @author lengleng
	 * @date 2025/05/30
	 */
	private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {

		/**
		 * 序列化版本号
		 */
		@Serial
		private static final long serialVersionUID = 1L;

		/**
		 * 令牌声明信息集合
		 * 存储令牌的所有声明，包括标准声明和自定义声明
		 */
		private final Map<String, Object> claims;

		/**
		 * 构造 OAuth2 访问令牌声明
		 * <p>
		 * 创建一个包含声明信息的访问令牌对象。
		 * 该构造函数将基本令牌信息与声明信息组合在一起。
		 * 
		 * @param tokenType 令牌类型，通常为 Bearer
		 * @param tokenValue 令牌值，即随机生成的不透明字符串
		 * @param issuedAt 颁发时间
		 * @param expiresAt 过期时间
		 * @param scopes 权限范围集合
		 * @param claims 声明信息映射，包含所有令牌相关的声明
		 */
		private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
				Set<String> scopes, Map<String, Object> claims) {
			super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
			this.claims = claims;
		}

		/**
		 * 获取 claims 集合
		 * <p>
		 * 返回与该令牌关联的所有声明信息。
		 * 这些声明将被存储在授权服务器中，并在令牌内省时使用。
		 * 
		 * @return claims 键值对集合，包含所有令牌声明
		 */
		@Override
		public Map<String, Object> getClaims() {
			return this.claims;
		}

	}

}
